package core;

import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;

import persistence.ContainerDTO;
import persistence.DataPersistor;

import core.exception.IllegalAddException;
import core.exception.IllegalRemoveException;
import core.exception.NotFoundException;
import core.exception.SaveException;
import core.reports.IReportVisitor;


/**The Container class is a Composite. A Container has child containers, items,
 * and products. This is the parent class for StorageUnit, ProductGroup, and
 * RootStorageUnit. The Container class has three abstract methods that children
 * must implement:
 * 
 * Container addContainer(String name). The child class determines what type
 * of container to add.
 *
 * Boolean canAddItem(Item item). This is called to determine if items can
 * be added, moved, or transferred into the container.
 * 
 * Boolean canAddProduct(Product product). This is called to determine if 
 * products can be added.
 * 
 * @author Natasha
 *
 */
public abstract class Container extends ModelObject implements Comparable<Container> {
	private static final long serialVersionUID = -4830820190803942579L;
	
	// These variables are protected because subclasses need to access them,
	// but other package classes should not use them.
	protected Map<String, Container> _children;
	protected String _name;
	protected Container _parent;
	protected StorageUnit _storageUnit; 
	private int _id;
	
	///////////////////////////////
	// Constructors
	//////////////////////////////
	
	/**Construct an empty container. In principle, a container is only useful if 
	 * constructed by a parent through the addContainer(String name) method.
	 * 
	 * @param name The name of the container. Names must be nonempty and unique 
	 * within the parent container.
	 * @throws IllegalArgumentException
	 */
	protected Container(String name) throws IllegalArgumentException {
		// check preconditions
		if (name.isEmpty())
			throw new IllegalArgumentException("Name may not be empty");
		
		_name = name;
		_parent = null;
		_storageUnit = null;
		_children = new TreeMap<String, Container>();
		_id = -1;
		
		//nothing contained initially
	}
	
	/////////////////////////////////
	// Name
	////////////////////////////////
	/**Retrieve the name of the container
	 * 
	 * @return The name of the container
	 */
	public String getName() {
		// this method has no preconditions
		return _name;
	}
	
	/////////////////////////////////
	// Children
	////////////////////////////////
	
	/**Test if a child container can be added with the given name.
	 * 
	 * @param name The name for the child container. The name must be
	 * nonempty and unique within the parent container.
	 * @return True if the name is valid, False otherwise.
	 */
	public boolean canAddContainer(String name) {
		// this method has no preconditions
		return name != null && !name.isEmpty() && !(_children.containsKey(name));
	}
	
	/**Add a child container to this container with the given name. 
	 * Child classes determine what type of container is created. For example,
	 * StorageUnits hold only ProductGroup children, and so a StorageUnit
	 * will create a ProductGroup child of the given name.
	 * 
	 * @param name The name of the new child container. The name must be
	 * nonempty and unique within this container.
	 * @throws IllegalAddException
	 * @return The new Container added.
	 */
	protected abstract Container addContainer(String name) throws IllegalAddException;
	
	/**Test if a child container can be removed. A child container must exist
	 * and contain no items. If it has children, it's children must also contain
	 * no items. The child container (and it's children) may contain products.
	 * 
	 * @param name The name of the child container.
	 * @return True if the child could be removed, False otherwise.
	 */
	public boolean canRemoveContainer(String name) {
		// This method has no preconditions
		Container child = _children.get(name);
		
		// a child must exit to be removed
		if (child == null)
			return false;
		
		// A child and all it's descendants must not contain items.
		return !containsItemsRecursive(child);
	}
	
	private static boolean containsItemsRecursive(Container c){
		// Check Preconditions
		assert(c != null);
		
		Iterator<Item> items = ItemManager.instance().itemsInContainer(c);
		if (items != null && items.hasNext())
			return true; // The container has items.
		
		Iterator<Container> childIter = c._children.values().iterator();
		while (childIter.hasNext()){
			if(containsItemsRecursive(childIter.next()))
				return true; // Some child has an item.
		}
		return false;
	}

	/**Remove the child container. The child container must exist, and it and it's
	 * children may not contain any items. When the child container is deleted,
	 * all of it's children will be deleted as well. The child container and it's
	 * children are allowed to contain products.
	 * 
	 * @param name The name of the child container to remove.
	 * @throws IllegalRemoveException
	 * @throws SaveException if can't save
	 */
	protected void removeContainer(String name) throws IllegalRemoveException, SaveException {
		// check preconditions
		if (!canRemoveContainer(name))
			throw new IllegalRemoveException("Illegal Remove Container");
		
		Container child = _children.get(name);
		
		// Deleting a child deletes all of it's descendants, so build
		// a set of descendants for the child.
		Collection<Container> descendants = new TreeSet<Container>();
		descendants.add(child);
		buildDescendantsRecursive(child,descendants);
		
		// if the child is not the storage unit, then the storage
		// unit needs to be told about all the removed containers.
		StorageUnit su = child.getStorageUnit();
		if (child != su){
			su.updateDescendants(descendants);
		}
		
		// Notify ItemManager and ProductManager of all the removed containers.
		ProductManager.instance().removeContainersFromAllProducts(descendants);
		ItemManager.instance().removeReferencesToContainer(descendants);
		
		// Save
		Iterator<Container> iter = descendants.iterator();
		DataPersistor.instance().getTransactionManager().beginTransaction();
		while(iter.hasNext()){
			ContainerDTO dto = new ContainerDTO(iter.next());
			DataPersistor.instance().getContainerDAO().removeContainer(dto);	

		}
		DataPersistor.instance().getTransactionManager().endTransaction(true);		
		
		// Finally, remove child from this parent. Now, all references to the child
		// should be gone, and java garbage collection should be able to handle the rest.
		_children.remove(name);
		
	}
	
	private static void buildDescendantsRecursive(Container c, Collection<Container> descendants){
		// check preconditions
		assert(c != null);
		assert(descendants != null);
		
		Iterator<Container> childIter = c._children.values().iterator();
		while (childIter.hasNext()){
			Container child = childIter.next();
			descendants.add(child);
			buildDescendantsRecursive(child,descendants);
		}		
	}
	
	/**Test if the newName is a valid name for the child container of the oldName.
	 * The container of the oldName must exist, and newName must be nonempty and 
	 * unique within the parent container.
	 * 
	 * @param oldName The current name of the child container.
	 * @param newName The new name for the child container. This must be nonempty
	 * and unique within the parent container.
	 * @return True if the newName is valid, False otherwise.
	 */
	public boolean canSetChildContainerName(String oldName, String newName){
		// This method has no preconditions.
		
		Container child = _children.get(oldName);
		
		// Cannot change a child that doesn't exist.
		if(child == null)
			return false;
		
		// If newname = oldname, then go ahead and say yes
		if (oldName.equals(newName))
			return true;
		
		// The new name cannot be empty
		if (newName == null || newName.isEmpty())
			return false;
		
		// The new name must be unique within the parent container (this).
		return !(_children.containsKey(newName));
	}
	
	/**Change the name of the child with oldName to newName. newName must be 
	 * nonempty and unique within this Container.
	 * 
	 * @param oldName The old name of the child container.
	 * @param newName The new name of the child container. This must be nonempty
	 * and unique within this container.
	 * @throws IllegalArgumentException
	 */
	protected void setChildContainerName(String oldName, String newName) 
			throws IllegalArgumentException {
		// check precondition
		if (!canSetChildContainerName(oldName,newName))
			throw new IllegalArgumentException("Illegal set child container name");
		
		// optimization
		if (oldName.equals(newName))
			return;
		
		// remove the child from the map (removing oldName);
		Container child = _children.remove(oldName);
		
		// just double checking
		assert(child != null);
		
		// change child's name
		child._name = newName;
		
		// add child back to map
		_children.put(newName, child);
			
	}	
	
	/**Return an iterator to the child containers
	 * 
	 * @return An iterator to the child containers.
	 */
	public Iterator<Container> getChildContainers() {
		// this method has no preconditions.
		return _children.values().iterator();
	}
	
	/**Determine if this container contains a child container of the given
	 * name.
	 * 
	 * @param name The name of the child container.
	 * @return True if this container has a child of that name, False otherwise.
	 */
	boolean containsContainer(String name){
		// This method has no preconditions
		if (name == null)
			return false;
		return _children.containsKey(name);
	}
	
	/**Determine if this container contains a the given child container.
	 * 
	 * @param container The child container
	 * @return True if this container has that child, False otherwise.
	 */
	boolean containsContainer(Container container){
		// This method has no preconditions
		return _children.containsValue(container);
	}
	
	/**Lookup the Container with the given name and return it. This may return
	 * null if no Container with the given name exists among the children.
	 * 
	 * @param name The name of the Container to find.
	 * @return The Container, if it exists, and null otherwise.
	 */
	public Container lookupContainer(String name){
		if (name == null)
			return null;
		return _children.get(name);
	}
	
	///////////////////////////
	// Parents
	//////////////////////////
	
	/**Return this Containers parent. This method may return null,
	 * since not all Containers have a parent (specifically, the
	 * RootStorageUnit does not have a parent).
	 * 
	 * @return The Container parent.
	 */
	public Container getParent(){
		return _parent;
	}
	
	/**Return the StorageUnit at the head of the tree. This may return
	 * null if there is not a parent StorageUnit (like for the RootStorageUnit).
	 * A StorageUnit will return a pointer to itself.
	 * 
	 * @return The StorageUnit at the head of the tree.
	 */
	public StorageUnit getStorageUnit(){
		return _storageUnit;
	}
	
	
	///////////////////////////
	// Products
	///////////////////////////
	
	/**Return an iterator to loop over the products contained in this Container.
	 * The iterator will be null if there are no items.
	 * 
	 * @return An iterator to loop over the products in this Container.
	 */
	public Iterator<Product> getProducts() {
		try {
			return ProductManager.instance().getProductsForContainer(this);
		} catch (NotFoundException e) {
			return null;
		}
	}	
	
	/**Return true if this container contains the product..
	 * Note, this function is not recursive (it does not check to see if children
	 * contain the product).
	 * 
	 * @param product The product to test for.
	 * @return True if the product is inside this container, False otherwise.
	 */
	public boolean containsProduct(Product product){
		// This method has no preconditions
		return ProductManager.instance().productContainsContainer(product, this);
	}
	
	////////////////////////////
	// Items
	////////////////////////////
	
	/**Return an iterator to loop over all items contained in this container.
	 * 
	 * @return An iterator to loop over all items contained in this container.
	 */
	public Iterator<Item> getItems() {
		// There are no preconditions
		return ItemManager.instance().itemsInContainer(this);
	}
	
	/**Return an iterator to loop over all items contained in this container of 
	 * the given product. This function is not recursive (it does not look to see
	 * if a subcontainer has this product).
	 * 
	 * @param product The product
	 * @return An iterator to loop over all items contained in this container.
	 */
	public Iterator<Item> getItems(Product product) {
		// This method has no preconditions.
		if (product == null)
			return null;
		return ItemManager.instance().itemsInProductAndContainer(this, product);
	}	
	
	/**Test if the item is contained in this container.
	 * 
	 * @param item The item.
	 * @return True if the item is contained in this container, False otherwise
	 */
	public boolean containsItem(Item item){
		// This method has no preconditions
		return  item != null && item.getContainer() == this;
	}

	public int compareTo(Container arg0) {
		if (this == arg0)
			return 0;
		// a container is equal if it's name and parent are equal
		if (this._name.equals(arg0._name) && this._parent == arg0._parent)
			return 0;
		
		// if the names are equal but they're not the same, check the parents.
		// the parents cannot be equal because then there would be duplicate names
		// in the same parent.
		if (this._name.equals(arg0._name)){
			if (this._parent != null)
				return this._parent.compareTo(arg0._parent);	
			else
				return -1;
		}				
		else
			return this._name.compareTo(arg0._name);
	}
	
	/**
	 * Accepts a Visitor
	 * @param v the visiting IReportVisitor
	 */
	public abstract void accept(IReportVisitor v);
	
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((_parent == null) ? 0 : _parent.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Container other = (Container) obj;
		if (_name == null) {
			if (other._name != null)
				return false;
		} else if (!_name.equals(other._name))
			return false;
		if (_parent == null) {
			if (other._parent != null)
				return false;
		} else if (!_parent.equals(other._parent))
			return false;
		return true;
	}

	public String toString() {
		return _name;
	}
	
	/**
	 * Return this containers id
	 * @return This containers id.
	 */
	public int getID(){
		return _id;
	}
	
	/**
	 * Set this containers id.
	 * @param id This id should be greater than/equal to 0. No checking
	 * is done to ensure it is unique.
	 */
	public void setID(int id){
		assert(id >= 0);
		_id = id;
	}
	
}
